import { IDBSchema } from './types'
import { IDBStore } from './store'

export class IDB {
  private db: IDBDatabase | null = null
  private stores: Array<IDBStore> = []
  private name: string
  private version: number
  private schema: IDBSchema

  constructor(name: string, version: number, schema: IDBSchema) {
    this.name = name
    this.version = version
    this.schema = schema
  }

  public getDB() {
    if (!this.db) {
      throw new Error('DB not ready')
    }
    return this.db
  }

  public getStore(storeName: string) {
    return this.stores.find((store) => store.storeName === storeName)
  }

  // 打开 db
  public open() {
    const request = window.indexedDB.open(this.name, this.version)
    const schema = this.schema
    console.log('db::opening')
    return new Promise<void>((resolve, reject) => {
      request.onerror = (event) => {
        console.log('db::open failed', event)
        const target = event.target as IDBOpenDBRequest
        reject(target.error)
      }
      request.onsuccess = (event) => {
        console.log('db::open success', event)
        // 如果打开成功，把数据库对象保存下来，以后增删改查都需要用到。
        const target = event.target as IDBOpenDBRequest
        // console.log()
        this.db = target.result
        // this.db.removeEventListener('versionchange', () => this.triggerDBCloseEvt.bind(this))
        // this.db.addEventListener('versionchange', () => this.triggerDBCloseEvt.bind(this))
        this.db.removeEventListener('close', this.triggerDBCloseEvt.bind(this))
        this.db.addEventListener('close', this.triggerDBCloseEvt.bind(this))
        schema.forEach((item) => {
          const { storeName } = item
          this.stores.push(new IDBStore(storeName, item, target.result))
        })
        resolve()
      }
      request.onupgradeneeded = (event) => {
        /**
         * 如果故意在文件系统里篡改了数据文件, 再次打开也会触发 onupgradeneeded, 但是打开动作依旧能触发 onsuccess 成功
         *
         * event.dataLoss => 'total'
         * event.dataLostMessage => "IndexedDB (database was corrupt): checksum mismatch"
         * event.newVersion => 1
         * event.oldVersion => 0
         * event.target.result => IDBDatabase {name: 'nim-accid', version: 1, objectStor...}
         */
        console.log('db::open onupgradeneeded', event)
        const target = event.target as IDBOpenDBRequest
        this.upgradeDBBySchema(target, schema)
      }
      request.onblocked = (event) => {
        /**
         * 维持一个 tab 版本是 1, 然后打开另一个 tab 要升级到 2. 另一个 tab 可能出现如下错误
         *
         * event.newVersion => 2
         * event.oldVersion => 1
         * event.target.readyState => 'pending'
         * event.target.result =>  [Exception: InvalidStateError: Failed to read the 'result' property from 'IDBRequest': The request has not finished. at IDBOpenDBRequest.invokeGetter (<anonymous>:3:28)]
         */
        const target = event.target as IDBOpenDBRequest
        console.log('db::open onblocked', event, target.result.toString())
        reject(target.result)
      }
    })
  }

  // 触发了 db 意外关闭的事件
  private triggerDBCloseEvt() {
    // db 连接意外关闭, 需要重启恢复
    console.warn('db::close event trigger')
    try {
      this.db && this.db.close()
    } catch (error) {
      console.warn('db::close previous db failed', error)
    }
    this.stores = []
    this.db = null
    this.open()
  }

  // 用 schema 来升级 db 结构
  private upgradeDBBySchema(target: IDBOpenDBRequest, schema: IDBSchema) {
    const db = target.result
    schema.forEach((item) => {
      const { storeName, primaryKey, indexes = [] } = item
      const objectStore =
        target.transaction && db.objectStoreNames.contains(storeName) ? target.transaction.objectStore(storeName) : db.createObjectStore(storeName, primaryKey)
      indexes.forEach((index) => {
        const { keyName, keyPath = keyName, unique = false, multiEntry = false } = index
        try {
          objectStore.index(keyName)
        } catch (e) {
          objectStore.createIndex(keyName, keyPath, { unique, multiEntry })
        }
      })
    })
  }

  // 关闭 db, 释放所有 store 的引用
  public destroy() {
    if (!this.db) {
      return
    }
    this.stores.forEach((store) => {
      store.destroy()
    })
    this.stores = []
    this.db.removeEventListener('close', this.triggerDBCloseEvt.bind(this))
    this.db.close()
    this.db = null
  }
}
